DRF Serializers

Von Basics zu Advanced Techniques

Arbeiten mit Serializern und ModelSerializer

Serializers • Validierung • Relations • Nested Objects

📋 Was lernen wir heute?

1

Serializer Grundlagen

Was sind Serializers?

Warum brauchen wir sie?

2

Serializer vs ModelSerializer

Unterschiede verstehen

Wann was nutzen?

3

Validierung

Field-Level Validation

Object-Level Validation

Custom Validators

4

Relations & Nested

ForeignKey Relations

Many-to-Many

Nested Serializers

5

Advanced Techniques

SerializerMethodField

Custom Fields

Best Practices

🔄 Was ist ein Serializer?

Serializer = Daten-Übersetzer

Konvertiert zwischen komplexen Datentypen und nativen Python-Typen

🎯 Hauptaufgaben

  • Serialisierung: Python-Objekte → JSON/XML
  • Deserialisierung: JSON/XML → Python-Objekte
  • Validierung: Eingabedaten prüfen
  • Objekterstellung: Neue Instanzen erstellen
  • Aktualisierung: Bestehende Objekte ändern

📊 Workflow

# 1. Serialisierung (Read - GET)
Python Object → Serializer → JSON
Movie(id=1, title="Matrix") → {"id": 1, "title": "Matrix"}

# 2. Deserialisierung (Write - POST/PUT)
JSON → Serializer → Python Object
{"title": "Inception"} → Movie(title="Inception")

❌ Ohne Serializer

# Manuell JSON erstellen
def movie_to_json(movie):
    return {
        'id': movie.id,
        'title': movie.title,
        'year': movie.year,
        # ... für jedes Feld
    }

# Manuell validieren
def validate_movie(data):
    if not data.get('title'):
        raise ValueError("Title required")
    if data.get('year') < 1888:
        raise ValueError("Year too early")
    # ... für jedes Feld

✅ Mit Serializer

# Automatisch!
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'

# Verwendung:
serializer = MovieSerializer(movie)
json_data = serializer.data  # Fertig!

# Validierung automatisch:
serializer = MovieSerializer(data=request.data)
if serializer.is_valid():
    serializer.save()  # Fertig!

⚖️ Serializer vs ModelSerializer

Zwei Arten von Serializern

Basis-Serializer vs. Model-basierter Serializer

🔧 Serializer (Basis)

Für nicht-Model Daten

from rest_framework import serializers

class MovieSerializer(serializers.Serializer):
    """Basis Serializer - Alles manuell"""
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    year = serializers.IntegerField()
    rating = serializers.DecimalField(
        max_digits=3, 
        decimal_places=1,
        required=False
    )
    
    def create(self, validated_data):
        """Manuell erstellen"""
        return Movie.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        """Manuell aktualisieren"""
        instance.title = validated_data.get('title', instance.title)
        instance.year = validated_data.get('year', instance.year)
        instance.rating = validated_data.get('rating', instance.rating)
        instance.save()
        return instance

✅ Vorteile:

  • Volle Kontrolle
  • Für non-Model Daten
  • Custom Logik möglich

❌ Nachteile:

  • Viel Code
  • Wiederholungen
  • Fehleranfällig

🎯 ModelSerializer

Für Django Models

from rest_framework import serializers

class MovieSerializer(serializers.ModelSerializer):
    """ModelSerializer - Automatisch!"""
    class Meta:
        model = Movie
        fields = '__all__'
        # Oder:
        # fields = ['id', 'title', 'year', 'rating']
        # exclude = ['created_at']
        read_only_fields = ['id', 'created_at', 'updated_at']

# Fertig! create() und update() automatisch!
# Validierung basierend auf Model-Definition!
# Felder automatisch aus Model!

✅ Vorteile:

  • Minimaler Code (~5 Zeilen)
  • Model-basiert (DRY)
  • create/update automatisch
  • Validierung aus Model

❌ Nachteile:

  • Nur für Models
  • Weniger flexibel

🎯 Wann was nutzen?

  • ModelSerializer: 95% der Fälle (wenn Django Model vorhanden)
  • Serializer: Login-Daten, externe APIs, Formulare ohne Model

📝 ModelSerializer - fields Optionen

1. Alle Felder

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'  # Alle Model-Felder

# Output:
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": "8.7",
  "description": "...",
  "created_at": "2024-01-01T10:00:00Z",
  "updated_at": "2024-01-01T10:00:00Z"
}

2. Bestimmte Felder (Include)

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = ['id', 'title', 'year', 'rating']
        # Nur diese Felder!

# Output:
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "rating": "8.7"
}

3. Felder ausschließen (Exclude)

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        exclude = ['created_at', 'updated_at']
        # Alle außer diese!

# Output:
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": "8.7",
  "description": "..."
}

4. Read-Only Felder

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']
        # Diese können NICHT geändert werden

# Bei POST/PUT ignoriert:
{
  "id": 999,  # ← Ignoriert!
  "title": "New Movie",
  "year": 2024
}

5. Extra Kwargs

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        extra_kwargs = {
            'title': {'required': True, 'min_length': 3},
            'year': {'min_value': 1888, 'max_value': 2100},
            'rating': {'required': False},
            'description': {'write_only': True}  # Nur bei POST/PUT
        }

6. Depth (Auto-Nested)

class MovieCastingSerializer(serializers.ModelSerializer):
    class Meta:
        model = MovieCasting
        fields = '__all__'
        depth = 1  # Auto-expand ForeignKeys!

# Output:
{
  "id": 1,
  "movie": {  # ← Automatisch verschachtelt!
    "id": 1,
    "title": "The Matrix"
  },
  "artist": {
    "id": 1,
    "first_name": "Keanu",
    "last_name": "Reeves"
  },
  "role_name": "Neo"
}

✅ Validierung in Serializern

3 Stufen der Validierung

Von einfach zu komplex

1

🔹 Field-Level Validation

Einzelne Felder validieren

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def validate_year(self, value):
        """Validiere year-Feld"""
        if value < 1888:
            raise serializers.ValidationError(
                "Erstes Film war 1888!"
            )
        if value > 2100:
            raise serializers.ValidationError(
                "Jahr zu weit in der Zukunft!"
            )
        return value
    
    def validate_title(self, value):
        """Validiere title-Feld"""
        if len(value) < 2:
            raise serializers.ValidationError(
                "Titel zu kurz!"
            )
        if Movie.objects.filter(title__iexact=value).exists():
            raise serializers.ValidationError(
                "Film existiert bereits!"
            )
        return value

Methode: validate_<field_name>(self, value)

2

🔸 Object-Level Validation

Mehrere Felder zusammen validieren

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def validate(self, attrs):
        """Validiere das gesamte Objekt"""
        # attrs = dict mit allen Feldern
        
        # Beispiel: Jahr und Rating zusammen prüfen
        year = attrs.get('year')
        rating = attrs.get('rating')
        
        if year and year < 1920 and rating and rating > 9.0:
            raise serializers.ValidationError(
                "Stummfilme können nicht so hoch bewertet sein!"
            )
        
        # Beispiel: Genre und Titel zusammen
        genre = attrs.get('genre')
        title = attrs.get('title')
        
        if genre == 'Horror' and 'Kids' in title:
            raise serializers.ValidationError(
                "Horror-Filme nicht für Kinder!"
            )
        
        return attrs  # Wichtig: attrs zurückgeben!

Methode: validate(self, attrs)

3

🔶 Custom Validators

Wiederverwendbare Validatoren

# filepath: movies/validators.py
from rest_framework import serializers

def validate_year_range(value):
    """Wiederverwendbarer Validator"""
    if value < 1888 or value > 2100:
        raise serializers.ValidationError(
            f"Jahr muss zwischen 1888 und 2100 liegen, nicht {value}"
        )

def validate_rating_range(value):
    """Rating-Validator"""
    if value < 0 or value > 10:
        raise serializers.ValidationError(
            "Rating muss zwischen 0 und 10 liegen"
        )


# filepath: movies/serializers.py
class MovieSerializer(serializers.ModelSerializer):
    # Validators direkt am Feld
    year = serializers.IntegerField(validators=[validate_year_range])
    rating = serializers.DecimalField(
        max_digits=3,
        decimal_places=1,
        validators=[validate_rating_range],
        required=False
    )
    
    class Meta:
        model = Movie
        fields = '__all__'

Wiederverwendbar in mehreren Serializern!

🛠️ Validierung - Praxis-Beispiele

Kompletter Movie Serializer mit allen Validierungen:

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie
from .validators import validate_year_range, validate_rating_range
from datetime import datetime


class MovieSerializer(serializers.ModelSerializer):
    """
    Movie Serializer mit umfassender Validierung
    """
    # Custom Validators
    year = serializers.IntegerField(validators=[validate_year_range])
    rating = serializers.DecimalField(
        max_digits=3,
        decimal_places=1,
        validators=[validate_rating_range],
        required=False,
        allow_null=True
    )
    
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']
        extra_kwargs = {
            'title': {
                'required': True,
                'min_length': 2,
                'max_length': 200
            },
            'description': {
                'required': False,
                'allow_blank': True
            }
        }
    
    # Field-Level Validation
    def validate_title(self, value):
        """Titel darf nicht bereits existieren"""
        # Bei Update: Eigenen Film ausschließen
        if self.instance:
            # Update-Modus
            if Movie.objects.filter(
                title__iexact=value
            ).exclude(pk=self.instance.pk).exists():
                raise serializers.ValidationError(
                    f"Film mit Titel '{value}' existiert bereits!"
                )
        else:
            # Create-Modus
            if Movie.objects.filter(title__iexact=value).exists():
                raise serializers.ValidationError(
                    f"Film mit Titel '{value}' existiert bereits!"
                )
        
        return value.strip()  # Leerzeichen entfernen
    
    def validate_genre(self, value):
        """Genre muss in erlaubter Liste sein"""
        allowed_genres = [
            'Action', 'Comedy', 'Drama', 'Horror', 
            'Sci-Fi', 'Romance', 'Thriller', 'Documentary'
        ]
        
        if value and value not in allowed_genres:
            raise serializers.ValidationError(
                f"Genre muss eines sein von: {', '.join(allowed_genres)}"
            )
        
        return value
    
    # Object-Level Validation
    def validate(self, attrs):
        """Gesamtes Objekt validieren"""
        year = attrs.get('year')
        rating = attrs.get('rating')
        genre = attrs.get('genre')
        title = attrs.get('title', '')
        
        # Jahr darf nicht in der Zukunft liegen
        current_year = datetime.now().year
        if year and year > current_year + 1:
            raise serializers.ValidationError({
                'year': f'Jahr darf nicht mehr als {current_year + 1} sein'
            })
        
        # Alte Filme können nicht so hoch bewertet sein
        if year and year < 1920 and rating and rating > 9.5:
            raise serializers.ValidationError(
                'Stummfilme vor 1920 können maximal 9.5 haben'
            )
        
        # Genre und Titel Kombination
        if genre == 'Documentary' and rating and rating > 9.0:
            # Warnung, aber kein Fehler
            pass
        
        # Horror nicht für Kids
        if genre == 'Horror' and any(word in title.lower() for word in ['kids', 'children', 'family']):
            raise serializers.ValidationError(
                'Horror-Filme nicht für Kinder geeignet'
            )
        
        return attrs

🚨 Validierung - Error Handling

Wie Validierungsfehler in Views behandeln:

# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import MovieSerializer


class MovieCreateView(APIView):
    def post(self, request):
        """Film erstellen mit Fehlerbehandlung"""
        serializer = MovieSerializer(data=request.data)
        
        # Validierung
        if serializer.is_valid():
            # ✅ Daten OK → Speichern
            movie = serializer.save()
            return Response(
                serializer.data,
                status=status.HTTP_201_CREATED
            )
        else:
            # ❌ Validierungsfehler → Error Response
            return Response(
                serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )

Error Response Format:

# Field-Level Error:
{
  "year": [
    "Jahr muss zwischen 1888 und 2100 liegen, nicht 1800"
  ],
  "title": [
    "Film mit Titel 'Matrix' existiert bereits!"
  ]
}

# Object-Level Error:
{
  "non_field_errors": [
    "Stummfilme vor 1920 können maximal 9.5 haben"
  ]
}

# Multiple Errors:
{
  "title": [
    "Dieses Feld ist erforderlich."
  ],
  "year": [
    "Jahr muss zwischen 1888 und 2100 liegen, nicht 2200"
  ],
  "rating": [
    "Rating muss zwischen 0 und 10 liegen"
  ]
}

💡 Best Practice:

  • Immer is_valid() vor save() aufrufen
  • Spezifische Fehlermeldungen (nicht "Invalid data")
  • Field-Level Errors für einzelne Felder
  • Object-Level Errors für Kombinationen

🔗 Relations - ForeignKey darstellung

Wie ForeignKeys serialisieren?

5 verschiedene Ansätze

1. PrimaryKeyRelatedField (Default)

# models.py
class MovieCasting(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
    role_name = models.CharField(max_length=200)

# serializers.py
class MovieCastingSerializer(serializers.ModelSerializer):
    class Meta:
        model = MovieCasting
        fields = '__all__'

# Output:
{
  "id": 1,
  "movie": 1,        # ← Nur ID!
  "artist": 5,       # ← Nur ID!
  "role_name": "Neo"
}

✅ Vorteile: Einfach, klein

❌ Nachteile: Client muss extra Request machen

2. StringRelatedField

class MovieCastingSerializer(serializers.ModelSerializer):
    movie = serializers.StringRelatedField()
    artist = serializers.StringRelatedField()
    
    class Meta:
        model = MovieCasting
        fields = '__all__'

# models.py braucht __str__:
class Movie(models.Model):
    def __str__(self):
        return self.title

# Output:
{
  "id": 1,
  "movie": "The Matrix",    # ← __str__() Ausgabe
  "artist": "Keanu Reeves", # ← __str__() Ausgabe
  "role_name": "Neo"
}

✅ Vorteile: Lesbar

❌ Nachteile: Read-only, keine IDs

3. SlugRelatedField

class MovieCastingSerializer(serializers.ModelSerializer):
    movie = serializers.SlugRelatedField(
        slug_field='title',  # Welches Feld?
        queryset=Movie.objects.all()
    )
    artist = serializers.SlugRelatedField(
        slug_field='last_name',
        queryset=Artist.objects.all()
    )
    
    class Meta:
        model = MovieCasting
        fields = '__all__'

# Output:
{
  "id": 1,
  "movie": "The Matrix",    # ← title-Feld
  "artist": "Reeves",       # ← last_name-Feld
  "role_name": "Neo"
}

✅ Vorteile: Lesbar, beschreibbar

❌ Nachteile: Feld muss unique sein

4. HyperlinkedRelatedField

class MovieCastingSerializer(serializers.ModelSerializer):
    movie = serializers.HyperlinkedRelatedField(
        view_name='movie-detail',
        queryset=Movie.objects.all()
    )
    
    class Meta:
        model = MovieCasting
        fields = '__all__'

# Output:
{
  "id": 1,
  "movie": "http://api.example.com/movies/1/",  # ← URL!
  "artist": "http://api.example.com/artists/5/",
  "role_name": "Neo"
}

✅ Vorteile: RESTful, direkt klickbar

❌ Nachteile: Längere URLs

5. Nested Serializer (Best!)

class MovieCastingSerializer(serializers.ModelSerializer):
    movie = MovieSerializer(read_only=True)
    artist = ArtistSerializer(read_only=True)
    
    # Für Write: IDs
    movie_id = serializers.PrimaryKeyRelatedField(
        source='movie',
        queryset=Movie.objects.all(),
        write_only=True
    )
    artist_id = serializers.PrimaryKeyRelatedField(
        source='artist',
        queryset=Artist.objects.all(),
        write_only=True
    )
    
    class Meta:
        model = MovieCasting
        fields = '__all__'

# Output (Read):
{
  "id": 1,
  "movie": {           # ← Verschachtelt!
    "id": 1,
    "title": "The Matrix",
    "year": 1999
  },
  "artist": {
    "id": 5,
    "first_name": "Keanu",
    "last_name": "Reeves"
  },
  "role_name": "Neo"
}

# Input (Write):
{
  "movie_id": 1,       # ← Nur IDs beim Schreiben
  "artist_id": 5,
  "role_name": "Neo"
}

✅ Vorteile: Volle Daten, best UX

❌ Nachteile: Größere Response

📦 Nested Serializers - Deep Dive

Verschachtelte Objekte serialisieren

Komplexe Datenstrukturen elegant darstellen

Beispiel: Movie mit Castings

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting


# 1. Artist Serializer (Basis)
class ArtistSerializer(serializers.ModelSerializer):
    full_name = serializers.SerializerMethodField()
    
    class Meta:
        model = Artist
        fields = ['id', 'first_name', 'last_name', 'full_name', 'birth_date']
    
    def get_full_name(self, obj):
        return f"{obj.first_name} {obj.last_name}"


# 2. MovieCasting Serializer (mit Artist nested)
class MovieCastingDetailSerializer(serializers.ModelSerializer):
    artist = ArtistSerializer(read_only=True)  # ← Nested!
    
    class Meta:
        model = MovieCasting
        fields = ['id', 'artist', 'role_name', 'is_lead']


# 3. Movie Serializer (mit Castings nested)
class MovieDetailSerializer(serializers.ModelSerializer):
    # Reverse Relation: related_name='castings'
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    casting_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def get_casting_count(self, obj):
        return obj.castings.count()

Output:

GET /api/movies/1/

{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": "8.7",
  "casting_count": 3,
  "castings": [                    # ← Nested Array!
    {
      "id": 1,
      "artist": {                  # ← Doppelt verschachtelt!
        "id": 5,
        "first_name": "Keanu",
        "last_name": "Reeves",
        "full_name": "Keanu Reeves",
        "birth_date": "1964-09-02"
      },
      "role_name": "Neo",
      "is_lead": true
    },
    {
      "id": 2,
      "artist": {
        "id": 6,
        "first_name": "Laurence",
        "last_name": "Fishburne",
        "full_name": "Laurence Fishburne",
        "birth_date": "1961-07-30"
      },
      "role_name": "Morpheus",
      "is_lead": true
    },
    {
      "id": 3,
      "artist": {
        "id": 7,
        "first_name": "Carrie-Anne",
        "last_name": "Moss",
        "full_name": "Carrie-Anne Moss",
        "birth_date": "1967-08-21"
      },
      "role_name": "Trinity",
      "is_lead": true
    }
  ]
}

✍️ Nested Write Operations

Problem: Nested Create/Update ist komplex!

DRF macht Nested READ automatisch, aber WRITE nicht!

❌ Problem: Standard ModelSerializer kann nur lesen

class MovieDetailSerializer(serializers.ModelSerializer):
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    #                                                   ^^^^^^^^^ Read-Only!
    
    class Meta:
        model = Movie
        fields = '__all__'

# POST geht NICHT:
{
  "title": "New Movie",
  "castings": [
    {"artist_id": 1, "role_name": "Hero"}  # ← Ignoriert!
  ]
}

✅ Lösung 1: create() überschreiben

class MovieDetailSerializer(serializers.ModelSerializer):
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    
    # Separate Felder für Write
    casting_data = serializers.ListField(
        child=serializers.DictField(),
        write_only=True,
        required=False
    )
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def create(self, validated_data):
        """Custom Create mit Nested Objects"""
        # 1. Casting-Daten extrahieren
        casting_data = validated_data.pop('casting_data', [])
        
        # 2. Movie erstellen
        movie = Movie.objects.create(**validated_data)
        
        # 3. Castings erstellen
        for casting in casting_data:
            MovieCasting.objects.create(
                movie=movie,
                artist_id=casting['artist_id'],
                role_name=casting['role_name'],
                is_lead=casting.get('is_lead', False)
            )
        
        return movie
    
    def update(self, instance, validated_data):
        """Custom Update mit Nested Objects"""
        # 1. Casting-Daten extrahieren
        casting_data = validated_data.pop('casting_data', None)
        
        # 2. Movie aktualisieren
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        
        # 3. Castings ersetzen (optional)
        if casting_data is not None:
            # Alte löschen
            instance.castings.all().delete()
            
            # Neue erstellen
            for casting in casting_data:
                MovieCasting.objects.create(
                    movie=instance,
                    artist_id=casting['artist_id'],
                    role_name=casting['role_name'],
                    is_lead=casting.get('is_lead', False)
                )
        
        return instance


# Verwendung:
POST /api/movies/
{
  "title": "Inception",
  "year": 2010,
  "casting_data": [  # ← Nested erstellen!
    {"artist_id": 10, "role_name": "Cobb", "is_lead": true},
    {"artist_id": 11, "role_name": "Ariadne", "is_lead": true}
  ]
}

🔄 Nested Write - Alternative Ansätze

✅ Lösung 2: Separate Endpoints (Empfohlen!)

# Besser: Zwei getrennte Requests

# 1. Movie erstellen
POST /api/movies/
{
  "title": "Inception",
  "year": 2010
}
# Response: {"id": 42, "title": "Inception", ...}

# 2. Castings hinzufügen
POST /api/movies/42/castings/
{
  "artist_id": 10,
  "role_name": "Cobb",
  "is_lead": true
}

POST /api/movies/42/castings/
{
  "artist_id": 11,
  "role_name": "Ariadne",
  "is_lead": true
}

Vorteile:

  • Einfacher Code
  • Klare Verantwortlichkeiten
  • Bessere Error-Handling
  • Granulare Permissions

✅ Lösung 3: Custom Action

# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    @action(detail=True, methods=['post'])
    def add_casting(self, request, pk=None):
        """Custom Action für Casting hinzufügen"""
        movie = self.get_object()
        
        # Validierung
        artist_id = request.data.get('artist_id')
        role_name = request.data.get('role_name')
        is_lead = request.data.get('is_lead', False)
        
        if not artist_id or not role_name:
            return Response(
                {'error': 'artist_id und role_name required'},
                status=400
            )
        
        # Casting erstellen
        casting = MovieCasting.objects.create(
            movie=movie,
            artist_id=artist_id,
            role_name=role_name,
            is_lead=is_lead
        )
        
        serializer = MovieCastingSerializer(casting)
        return Response(serializer.data, status=201)


# Verwendung:
POST /api/movies/42/add_casting/
{
  "artist_id": 10,
  "role_name": "Cobb",
  "is_lead": true
}

🎯 Best Practice: Wann was?

  • Separate Endpoints: Standard (90% der Fälle)
  • Custom Action: Spezielle Workflows
  • Nested Create: Nur wenn wirklich nötig (Import, Bulk)

Grund: Einfacherer Code, bessere Wartbarkeit, klarere API

💡 Empfehlung:

Nutze Nested Serializers für READ (viele Daten auf einmal)

Nutze Separate Endpoints für WRITE (klarere Struktur)

🎨 SerializerMethodField - Custom Felder

Berechnete Felder hinzufügen

Felder die nicht im Model existieren

Beispiel: Movie mit berechneten Feldern

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie
from datetime import datetime


class MovieSerializer(serializers.ModelSerializer):
    # Berechnete Felder
    age = serializers.SerializerMethodField()
    is_classic = serializers.SerializerMethodField()
    rating_category = serializers.SerializerMethodField()
    casting_count = serializers.SerializerMethodField()
    lead_actors = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = [
            'id', 'title', 'year', 'rating',
            'age', 'is_classic', 'rating_category',  # ← Custom!
            'casting_count', 'lead_actors'
        ]
    
    def get_age(self, obj):
        """Alter des Films in Jahren"""
        current_year = datetime.now().year
        return current_year - obj.year
    
    def get_is_classic(self, obj):
        """Ist der Film ein Klassiker? (älter als 30 Jahre)"""
        return self.get_age(obj) > 30
    
    def get_rating_category(self, obj):
        """Rating-Kategorie basierend auf Bewertung"""
        if not obj.rating:
            return "Nicht bewertet"
        
        rating = float(obj.rating)
        if rating >= 9.0:
            return "Meisterwerk"
        elif rating >= 8.0:
            return "Ausgezeichnet"
        elif rating >= 7.0:
            return "Gut"
        elif rating >= 6.0:
            return "Durchschnittlich"
        else:
            return "Schwach"
    
    def get_casting_count(self, obj):
        """Anzahl der Besetzungen"""
        return obj.castings.count()
    
    def get_lead_actors(self, obj):
        """Liste der Hauptdarsteller"""
        lead_castings = obj.castings.filter(is_lead=True)
        return [
            {
                'name': f"{c.artist.first_name} {c.artist.last_name}",
                'role': c.role_name
            }
            for c in lead_castings
        ]

Output:

GET /api/movies/1/

{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "rating": "8.7",
  "age": 25,                      # ← Berechnet!
  "is_classic": false,            # ← Berechnet!
  "rating_category": "Ausgezeichnet",  # ← Berechnet!
  "casting_count": 3,             # ← Berechnet!
  "lead_actors": [                # ← Berechnet!
    {"name": "Keanu Reeves", "role": "Neo"},
    {"name": "Laurence Fishburne", "role": "Morpheus"},
    {"name": "Carrie-Anne Moss", "role": "Trinity"}
  ]
}

🎯 SerializerMethodField:

  • Immer read-only
  • Methode: get_<field_name>(self, obj)
  • Gut für: Berechnungen, Aggregationen, Custom Logik
  • ⚠️ Performance: Wird für jedes Objekt ausgeführt!

⚡ SerializerMethodField - Performance-Optimierung

Problem: N+1 Queries!

SerializerMethodField kann langsam sein bei Listen

❌ Langsam: Ohne Optimierung

class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.SerializerMethodField()
    lead_actors = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = ['id', 'title', 'casting_count', 'lead_actors']
    
    def get_casting_count(self, obj):
        # ❌ Query für jedes Movie!
        return obj.castings.count()
    
    def get_lead_actors(self, obj):
        # ❌ Query für jedes Movie!
        lead_castings = obj.castings.filter(is_lead=True)
        return [c.artist.first_name for c in lead_castings]


# Problem bei Liste:
GET /api/movies/  # 100 Filme
# → 1 Query für Movies
# → 100 Queries für casting_count
# → 100 Queries für lead_actors
# = 201 Queries! 😱

✅ Schnell: Mit Optimierung

# 1. View optimieren:
from django.db.models import Count, Prefetch

class MovieViewSet(viewsets.ModelViewSet):
    serializer_class = MovieSerializer
    
    def get_queryset(self):
        """QuerySet optimieren"""
        return Movie.objects.annotate(
            # Casting-Count direkt in DB berechnen
            casting_count_db=Count('castings')
        ).prefetch_related(
            # Lead Castings vorab laden
            Prefetch(
                'castings',
                queryset=MovieCasting.objects.filter(
                    is_lead=True
                ).select_related('artist')
            )
        )


# 2. Serializer nutzt Prefetch:
class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.IntegerField(
        source='casting_count_db',  # ← Aus Annotation!
        read_only=True
    )
    lead_actors = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = ['id', 'title', 'casting_count', 'lead_actors']
    
    def get_lead_actors(self, obj):
        # ✅ Verwendet prefetch_related, keine extra Query!
        return [
            c.artist.first_name 
            for c in obj.castings.all()  # Schon geladen!
            if c.is_lead
        ]


# Resultat:
GET /api/movies/  # 100 Filme
# → 1 Query für Movies + Annotation
# → 1 Query für prefetch_related
# = 2 Queries! 🚀

💡 Performance Best Practices:

  • annotate() für Berechnungen in DB
  • select_related() für ForeignKeys
  • prefetch_related() für Many-to-Many/Reverse Relations
  • only() / defer() für weniger Felder
  • ❌ Niemals Queries in Schleifen (SerializerMethodField)

📖✍️ Read vs Write Serializers

Verschiedene Serializers für verschiedene Zwecke

Read: Viele Daten, Write: Nur Nötige

Pattern: Separate Serializers

# filepath: movies/serializers.py

# 1. LIST: Minimal (für Übersicht)
class MovieListSerializer(serializers.ModelSerializer):
    """Minimal für Listen"""
    class Meta:
        model = Movie
        fields = ['id', 'title', 'year', 'rating']


# 2. DETAIL: Maximal (für Einzelansicht)
class MovieDetailSerializer(serializers.ModelSerializer):
    """Maximal für Details mit Nested Objects"""
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    casting_count = serializers.SerializerMethodField()
    age = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def get_casting_count(self, obj):
        return obj.castings.count()
    
    def get_age(self, obj):
        from datetime import datetime
        return datetime.now().year - obj.year


# 3. CREATE/UPDATE: Nur beschreibbare Felder
class MovieWriteSerializer(serializers.ModelSerializer):
    """Nur für Create/Update"""
    class Meta:
        model = Movie
        fields = ['title', 'year', 'genre', 'rating', 'description']
        extra_kwargs = {
            'title': {'required': True},
            'year': {'required': True},
        }
    
    # Validierung...
    def validate_year(self, value):
        if value < 1888:
            raise serializers.ValidationError("Jahr zu früh")
        return value

ViewSet mit verschiedenen Serializers:

# filepath: movies/views.py
from rest_framework import viewsets

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    
    def get_serializer_class(self):
        """Dynamisch Serializer wählen"""
        if self.action == 'list':
            return MovieListSerializer  # Minimal
        
        elif self.action in ['create', 'update', 'partial_update']:
            return MovieWriteSerializer  # Nur Schreibfelder
        
        else:  # retrieve
            return MovieDetailSerializer  # Maximal


# Resultat:
GET /api/movies/           # → MovieListSerializer (klein)
GET /api/movies/1/         # → MovieDetailSerializer (groß, nested)
POST /api/movies/          # → MovieWriteSerializer (nur Pflichtfelder)
PUT /api/movies/1/         # → MovieWriteSerializer
PATCH /api/movies/1/       # → MovieWriteSerializer

🎨 Custom Fields

Eigene Feld-Typen erstellen

Wenn Standard-Felder nicht ausreichen

Beispiel 1: Duration Field (Zeitdauer)

# filepath: movies/fields.py
from rest_framework import serializers
from datetime import timedelta


class DurationField(serializers.Field):
    """Custom Field für Zeitdauer in Minuten"""
    
    def to_representation(self, value):
        """Python timedelta → JSON String"""
        if value is None:
            return None
        
        # timedelta → Minuten
        total_minutes = int(value.total_seconds() / 60)
        hours = total_minutes // 60
        minutes = total_minutes % 60
        
        if hours > 0:
            return f"{hours}h {minutes}min"
        else:
            return f"{minutes}min"
    
    def to_internal_value(self, data):
        """JSON String → Python timedelta"""
        if data is None:
            return None
        
        # Parse verschiedene Formate
        import re
        
        # Format: "120min" oder "2h 30min"
        pattern = r'(?:(\d+)h\s*)?(?:(\d+)min)?'
        match = re.match(pattern, str(data))
        
        if not match:
            raise serializers.ValidationError(
                "Format muss sein: '120min' oder '2h 30min'"
            )
        
        hours = int(match.group(1) or 0)
        minutes = int(match.group(2) or 0)
        
        total_minutes = hours * 60 + minutes
        
        if total_minutes <= 0:
            raise serializers.ValidationError(
                "Dauer muss größer als 0 sein"
            )
        
        return timedelta(minutes=total_minutes)


# Verwendung in Serializer:
class MovieSerializer(serializers.ModelSerializer):
    duration = DurationField()
    
    class Meta:
        model = Movie
        fields = ['id', 'title', 'duration']


# API:
GET /api/movies/1/
{
  "id": 1,
  "title": "The Matrix",
  "duration": "2h 16min"  # ← Custom Format!
}

POST /api/movies/
{
  "title": "New Movie",
  "duration": "90min"  # ← Wird zu timedelta
}

Beispiel 2: Color Field (Hex-Farben)

# filepath: movies/fields.py
class ColorField(serializers.Field):
    """Custom Field für Hex-Farben"""
    
    def to_representation(self, value):
        """DB String → JSON Hex"""
        if not value:
            return None
        
        # Sicherstellen dass # vorhanden
        if not value.startswith('#'):
            value = '#' + value
        
        return value.upper()
    
    def to_internal_value(self, data):
        """JSON Hex → DB String"""
        if not data:
            return None
        
        # # entfernen falls vorhanden
        color = data.replace('#', '')
        
        # Validierung: Muss 6 Hex-Zeichen sein
        import re
        if not re.match(r'^[0-9A-Fa-f]{6}$', color):
            raise serializers.ValidationError(
                "Farbe muss Hex-Format haben: #RRGGBB oder RRGGBB"
            )
        
        return color.upper()


# Verwendung:
class GenreSerializer(serializers.ModelSerializer):
    color = ColorField()
    
    class Meta:
        model = Genre
        fields = ['id', 'name', 'color']


# API:
GET /api/genres/1/
{
  "id": 1,
  "name": "Action",
  "color": "#FF0000"  # ← Immer mit #, uppercase
}

POST /api/genres/
{
  "name": "Comedy",
  "color": "00ff00"  # ← Akzeptiert verschiedene Formate
}
# Gespeichert als: "00FF00"

Beispiel 3: Email List Field

class EmailListField(serializers.Field):
    """Custom Field für Liste von E-Mails"""
    
    def to_representation(self, value):
        """DB String → JSON Array"""
        if not value:
            return []
        
        # Komma-separiert → Array
        return [email.strip() for email in value.split(',')]
    
    def to_internal_value(self, data):
        """JSON Array → DB String"""
        if not data:
            return ""
        
        # Validierung
        from django.core.validators import validate_email
        from django.core.exceptions import ValidationError as DjangoValidationError
        
        if not isinstance(data, list):
            raise serializers.ValidationError(
                "Muss eine Liste sein"
            )
        
        # Jede E-Mail validieren
        validated_emails = []
        for email in data:
            try:
                validate_email(email)
                validated_emails.append(email.strip())
            except DjangoValidationError:
                raise serializers.ValidationError(
                    f"'{email}' ist keine gültige E-Mail"
                )
        
        # Array → Komma-separiert
        return ','.join(validated_emails)


# Verwendung:
class MovieSerializer(serializers.ModelSerializer):
    contact_emails = EmailListField()
    
    class Meta:
        model = Movie
        fields = ['id', 'title', 'contact_emails']


# API:
GET /api/movies/1/
{
  "id": 1,
  "title": "The Matrix",
  "contact_emails": [  # ← Array
    "info@matrix.com",
    "support@matrix.com"
  ]
}

POST /api/movies/
{
  "title": "New Movie",
  "contact_emails": [  # ← Array
    "contact@example.com",
    "admin@example.com"
  ]
}
# DB: "contact@example.com,admin@example.com"

🔄 to_representation() - Response anpassen

Komplette Response-Struktur ändern

to_representation() wird für jedes Objekt aufgerufen

Beispiel 1: Zusätzliche Meta-Daten hinzufügen

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def to_representation(self, instance):
        """Response erweitern"""
        # Standard-Daten holen
        data = super().to_representation(instance)
        
        # Meta-Informationen hinzufügen
        data['_meta'] = {
            'created': instance.created_at.isoformat(),
            'updated': instance.updated_at.isoformat(),
            'url': f'/movies/{instance.id}/',
            'casting_count': instance.castings.count()
        }
        
        # Zusätzliche berechnete Felder
        from datetime import datetime
        current_year = datetime.now().year
        data['age'] = current_year - instance.year
        data['is_new'] = data['age'] < 5
        
        return data


# Output:
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "rating": "8.7",
  "age": 25,
  "is_new": false,
  "_meta": {
    "created": "2024-01-01T10:00:00Z",
    "updated": "2024-01-15T14:30:00Z",
    "url": "/movies/1/",
    "casting_count": 3
  }
}

Beispiel 2: Felder conditional anzeigen

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def to_representation(self, instance):
        """Felder je nach Kontext anzeigen"""
        data = super().to_representation(instance)
        
        # Request aus Context holen
        request = self.context.get('request')
        
        if request and not request.user.is_staff:
            # Normale User: Sensitive Daten entfernen
            data.pop('internal_notes', None)
            data.pop('production_cost', None)
        
        # Leere Felder entfernen
        data = {
            key: value 
            for key, value in data.items() 
            if value is not None and value != ''
        }
        
        # Rating formatieren
        if data.get('rating'):
            data['rating_stars'] = '⭐' * int(float(data['rating']))
        
        return data


# Output für normale User:
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "rating": "8.7",
  "rating_stars": "⭐⭐⭐⭐⭐⭐⭐⭐"
  # internal_notes und production_cost fehlen!
}

Beispiel 3: Nested Objekte filtern

class MovieDetailSerializer(serializers.ModelSerializer):
    castings = MovieCastingSerializer(many=True, read_only=True)
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def to_representation(self, instance):
        """Nur Lead-Actors anzeigen"""
        data = super().to_representation(instance)
        
        # Nur Hauptdarsteller
        if 'castings' in data:
            data['castings'] = [
                casting for casting in data['castings']
                if casting.get('is_lead')
            ]
            
            # Zusätzlich: Top 3 nach Reihenfolge
            data['castings'] = data['castings'][:3]
        
        return data


# Output:
{
  "id": 1,
  "title": "The Matrix",
  "castings": [  # Nur Top 3 Lead Actors!
    {"id": 1, "artist": "Keanu Reeves", "role_name": "Neo"},
    {"id": 2, "artist": "Laurence Fishburne", "role_name": "Morpheus"},
    {"id": 3, "artist": "Carrie-Anne Moss", "role_name": "Trinity"}
  ]
}

📥 to_internal_value() - Input anpassen

Eingabedaten vor-verarbeiten

to_internal_value() wird vor Validierung aufgerufen

Beispiel 1: Daten normalisieren

class MovieSerializer(serializers.ModelSerializer):
                    class Meta:
                        model = Movie
                        fields = '__all__'
                    
                    def to_internal_value(self, data):
                        """Input normalisieren"""
                        # Kopie erstellen (nicht Original ändern!)
                        data = data.copy()
                        
                        # Titel trimmen und kapitalisieren
                        if 'title' in data:
                            data['title'] = data['title'].strip()
                            # Ersten Buchstaben groß
                            if data['title']:
                                data['title'] = data['title'][0].upper() + data['title'][1:]
                        
                        # Genre normalisieren
                        if 'genre' in data:
                            genre_mapping = {
                                'scifi': 'Sci-Fi',
                                'sci fi': 'Sci-Fi',
                                'action': 'Action',
                                'horror': 'Horror',
                            }
                            data['genre'] = genre_mapping.get(
                                data['genre'].lower(), 
                                data['genre']
                            )
                        
                        # Rating: String → Decimal
                        if 'rating' in data and isinstance(data['rating'], str):
                            data['rating'] = data['rating'].replace(',', '.')
                        
                        # Standard-Verarbeitung
                        return super().to_internal_value(data)


# Input:
POST /api/movies/
{
  "title": "  the MATRIX  ",  # ← Unnötige Leerzeichen
  "genre": "scifi",            # ← Kleinbuchstaben
  "rating": "8,7"              # ← Komma statt Punkt
}

# Wird zu:
{
  "title": "The MATRIX",       # ← Getrimmt
  "genre": "Sci-Fi",           # ← Normalisiert
  "rating": "8.7"              # ← Punkt
}

Beispiel 2: Felder umbenennen

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def to_internal_value(self, data):
        """Felder von externem API-Format konvertieren"""
        data = data.copy()
        
        # Externe API nutzt andere Feldnamen
        field_mapping = {
            'movie_title': 'title',
            'release_year': 'year',
            'imdb_rating': 'rating',
            'category': 'genre',
        }
        
        # Felder umbenennen
        for old_name, new_name in field_mapping.items():
            if old_name in data:
                data[new_name] = data.pop(old_name)
        
        return super().to_internal_value(data)


# Input (externe API):
{
  "movie_title": "The Matrix",
  "release_year": 1999,
  "imdb_rating": 8.7,
  "category": "Sci-Fi"
}

# Wird zu (interne Felder):
{
  "title": "The Matrix",
  "year": 1999,
  "rating": 8.7,
  "genre": "Sci-Fi"
}

Beispiel 3: Default-Werte setzen

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    
    def to_internal_value(self, data):
        """Intelligente Default-Werte"""
        data = data.copy()
        
        # Wenn Jahr fehlt, aktuelles Jahr
        if 'year' not in data:
            from datetime import datetime
            data['year'] = datetime.now().year
        
        # Wenn Genre fehlt, aus Titel raten
        if 'genre' not in data and 'title' in data:
            title_lower = data['title'].lower()
            if any(word in title_lower for word in ['horror', 'scary', 'nightmare']):
                data['genre'] = 'Horror'
            elif any(word in title_lower for word in ['love', 'romance', 'heart']):
                data['genre'] = 'Romance'
            elif any(word in title_lower for word in ['space', 'future', 'alien']):
                data['genre'] = 'Sci-Fi'
            else:
                data['genre'] = 'Drama'  # Default
        
        # User aus Request holen und als created_by setzen
        request = self.context.get('request')
        if request and hasattr(request, 'user'):
            data['created_by'] = request.user.id
        
        return super().to_internal_value(data)


# Input:
{
  "title": "Alien from Space"
  # Kein year, kein genre!
}

# Wird zu:
{
  "title": "Alien from Space",
  "year": 2024,           # ← Aktuelles Jahr
  "genre": "Sci-Fi",      # ← Aus Titel erkannt
  "created_by": 5         # ← Aus Request
}

🎛️ Dynamic Fields - Flexible Serializer

Felder dynamisch ein-/ausblenden

Client entscheidet welche Felder er braucht

Pattern: Dynamic Fields Serializer

# filepath: movies/serializers.py
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    Basis-Serializer mit Dynamic Fields Support
    
    Verwendung:
    GET /api/movies/?fields=id,title,year
    """
    
    def __init__(self, *args, **kwargs):
        # Felder aus Request-Context holen
        context = kwargs.get('context', {})
        request = context.get('request')
        
        # Standard-Initialisierung
        super().__init__(*args, **kwargs)
        
        if not request:
            return
        
        # 1. ?fields=id,title,year → Nur diese Felder
        fields_param = request.query_params.get('fields')
        if fields_param:
            fields = fields_param.split(',')
            # Alle anderen entfernen
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)
        
        # 2. ?exclude=description,notes → Diese ausschließen
        exclude_param = request.query_params.get('exclude')
        if exclude_param:
            exclude = exclude_param.split(',')
            for field_name in exclude:
                self.fields.pop(field_name, None)


# Verwendung in Movie Serializer:
class MovieSerializer(DynamicFieldsModelSerializer):
    castings = MovieCastingSerializer(many=True, read_only=True)
    age = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def get_age(self, obj):
        from datetime import datetime
        return datetime.now().year - obj.year

Verwendung:

# 1. Alle Felder (Normal)
GET /api/movies/1/
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": "8.7",
  "description": "...",
  "age": 25,
  "castings": [...]
}

# 2. Nur bestimmte Felder
GET /api/movies/1/?fields=id,title,year
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999
}

# 3. Felder ausschließen
GET /api/movies/1/?exclude=description,castings
{
  "id": 1,
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": "8.7",
  "age": 25
  # description und castings fehlen!
}

# 4. Kombination
GET /api/movies/?fields=id,title,year,rating&exclude=description
# → Nur id, title, year, rating (exclude hat Vorrang)

✅ Vorteile:

  • Client holt nur benötigte Daten
  • Reduzierte Response-Größe
  • Bessere Performance
  • Flexible API

📱 Use-Cases:

  • Mobile: Weniger Daten = schneller
  • Tabellen: Nur Spalten die angezeigt werden
  • Listen: Minimal, Details: Maximal

🔄 Serializer Context - Daten weitergeben

Context = Extra-Daten für Serializer

Request, View, User, etc. an Serializer übergeben

1. Context in Views setzen

# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response

class MovieListView(APIView):
    def get(self, request):
        movies = Movie.objects.all()
        
        # Context explizit setzen
        serializer = MovieSerializer(
            movies,
            many=True,
            context={
                'request': request,      # Request-Objekt
                'view': self,            # View-Objekt
                'format': 'json',        # Format
                'custom_data': 'value'   # Eigene Daten!
            }
        )
        
        return Response(serializer.data)


# ViewSets setzen Context automatisch!
from rest_framework import viewsets

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    # Context automatisch:
    # {
    #   'request': request,
    #   'view': self,
    #   'format': format
    # }

2. Context in Serializer nutzen

class MovieSerializer(serializers.ModelSerializer):
    url = serializers.SerializerMethodField()
    is_favorite = serializers.SerializerMethodField()
    can_edit = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def get_url(self, obj):
        """Absolute URL mit Request"""
        request = self.context.get('request')
        if request is None:
            return None
        
        # Reverse URL generieren
        from django.urls import reverse
        path = reverse('movie-detail', kwargs={'pk': obj.pk})
        return request.build_absolute_uri(path)
    
    def get_is_favorite(self, obj):
        """Ist Favorit für aktuellen User?"""
        request = self.context.get('request')
        if not request or not request.user.is_authenticated:
            return False
        
        # Prüfen ob User diesen Film favorisiert hat
        return obj.favorites.filter(user=request.user).exists()
    
    def get_can_edit(self, obj):
        """Kann User diesen Film bearbeiten?"""
        request = self.context.get('request')
        if not request or not request.user.is_authenticated:
            return False
        
        # Owner oder Admin
        return (
            obj.created_by == request.user or 
            request.user.is_staff
        )


# Output:
GET /api/movies/1/
{
  "id": 1,
  "title": "The Matrix",
  "url": "http://example.com/api/movies/1/",  # ← Aus Request
  "is_favorite": true,                         # ← Für diesen User
  "can_edit": false                            # ← Für diesen User
}

3. Custom Context-Daten

# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def get_serializer_context(self):
        """Context erweitern"""
        context = super().get_serializer_context()
        
        # Custom Daten hinzufügen
        context['include_stats'] = self.request.query_params.get('stats', False)
        context['user_country'] = self.get_user_country()
        context['api_version'] = 'v2'
        
        return context
    
    def get_user_country(self):
        """User-Land aus IP ermitteln (Beispiel)"""
        # IP-Geolocation...
        return 'DE'


# Serializer:
class MovieSerializer(serializers.ModelSerializer):
    stats = serializers.SerializerMethodField()
    available_in_country = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
    
    def get_stats(self, obj):
        """Nur wenn ?stats=true"""
        if not self.context.get('include_stats'):
            return None
        
        return {
            'views': obj.view_count,
            'likes': obj.like_count,
            'comments': obj.comment_count
        }
    
    def get_available_in_country(self, obj):
        """Verfügbar in User-Land?"""
        country = self.context.get('user_country')
        return obj.available_countries.filter(code=country).exists()


# API:
GET /api/movies/1/?stats=true
{
  "id": 1,
  "title": "The Matrix",
  "stats": {           # ← Nur mit ?stats=true
    "views": 1000000,
    "likes": 50000,
    "comments": 1500
  },
  "available_in_country": true  # ← Basierend auf User-IP
}

💡 Serializer Best Practices

1. 📏 DRY - Don't Repeat Yourself

❌ Schlecht:
class MovieListSerializer(serializers.ModelSerializer):
    age = serializers.SerializerMethodField()
    
    def get_age(self, obj):
        from datetime import datetime
        return datetime.now().year - obj.year

class MovieDetailSerializer(serializers.ModelSerializer):
    age = serializers.SerializerMethodField()
    
    def get_age(self, obj):  # ← Duplikat!
        from datetime import datetime
        return datetime.now().year - obj.year

✅ Besser:
class MovieBaseSerializer(serializers.ModelSerializer):
    """Basis mit gemeinsamer Logik"""
    age = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = ['id', 'title', 'year', 'age']
    
    def get_age(self, obj):
        from datetime import datetime
        return datetime.now().year - obj.year

class MovieListSerializer(MovieBaseSerializer):
    class Meta(MovieBaseSerializer.Meta):
        fields = ['id', 'title', 'year']

class MovieDetailSerializer(MovieBaseSerializer):
    castings = MovieCastingSerializer(many=True)
    
    class Meta(MovieBaseSerializer.Meta):
        fields = '__all__'

2. 🎯 Single Responsibility

❌ Schlecht: Ein Serializer für alles
class MovieSerializer(serializers.ModelSerializer):
    # Viel zu viel Logik in einem Serializer!
    castings = ...
    stats = ...
    recommendations = ...
    reviews = ...
    # 500 Zeilen Code...

✅ Besser: Aufteilen
class MovieListSerializer(serializers.ModelSerializer):
    """Nur für Listen"""
    class Meta:
        model = Movie
        fields = ['id', 'title', 'year', 'rating']

class MovieDetailSerializer(serializers.ModelSerializer):
    """Nur für Details"""
    castings = MovieCastingSerializer(many=True)
    class Meta:
        model = Movie
        fields = '__all__'

class MovieStatsSerializer(serializers.ModelSerializer):
    """Nur für Statistiken"""
    stats = serializers.SerializerMethodField()
    # ...

3. ⚡ Performance

❌ Schlecht: N+1 Queries
class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.SerializerMethodField()
    
    def get_casting_count(self, obj):
        return obj.castings.count()  # ← Query pro Movie!

✅ Besser: Annotation in View
class MovieViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Movie.objects.annotate(
            casting_count_db=Count('castings')
        )

class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.IntegerField(
        source='casting_count_db'  # ← Aus Annotation!
    )

✅ Oder: Prefetch
class MovieViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Movie.objects.prefetch_related('castings')

class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.SerializerMethodField()
    
    def get_casting_count(self, obj):
        return obj.castings.count()  # ← Schon geladen!

4. 🔒 Security

❌ Schlecht: Alle Felder schreibbar
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    # User kann ALLES ändern: created_by, is_verified, etc.

✅ Besser: read_only_fields
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = [
            'id', 
            'created_at', 
            'updated_at',
            'created_by',      # ← Nicht änderbar!
            'is_verified',     # ← Nur Admin
            'view_count'       # ← Nur System
        ]

✅ Oder: Separate Write-Serializer
class MovieWriteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = ['title', 'year', 'genre', 'rating']
        # Nur diese Felder erlaubt!

5. ✅ Validierung

❌ Schlecht: Validierung in View
class MovieCreateView(APIView):
    def post(self, request):
        if not request.data.get('title'):
            return Response({'error': 'Title required'})
        if request.data.get('year') < 1888:
            return Response({'error': 'Year too early'})
        # ...

✅ Besser: Validierung im Serializer
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        extra_kwargs = {
            'title': {'required': True, 'min_length': 2}
        }
    
    def validate_year(self, value):
        if value < 1888:
            raise serializers.ValidationError("Year too early")
        return value
    
    def validate(self, attrs):
        # Multi-field Validierung
        if attrs['year'] < 1920 and attrs['rating'] > 9.5:
            raise serializers.ValidationError("Invalid combination")
        return attrs

6. 📝 Documentation

✅ Gut dokumentiert:
class MovieSerializer(serializers.ModelSerializer):
    """
    Serializer für Movie-Objekte.
    
    Fields:
        - id: Eindeutige ID (read-only)
        - title: Film-Titel (required, min 2 chars)
        - year: Erscheinungsjahr (1888-2100)
        - rating: IMDB Rating (0-10, optional)
        - age: Alter in Jahren (berechnet)
    
    Nested:
        - castings: Liste aller Besetzungen (read-only)
    
    Validierung:
        - Titel muss unique sein
        - Jahr nicht in Zukunft
        - Alte Filme max 9.5 Rating
    """
    age = serializers.SerializerMethodField()
    castings = MovieCastingSerializer(many=True, read_only=True)
    
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']
    
    def get_age(self, obj):
        """Berechne Alter des Films in Jahren"""
        from datetime import datetime
        return datetime.now().year - obj.year

🎯 Komplettes Beispiel - Production-Ready

Alle Konzepte zusammen

Ein vollständiger, production-ready Serializer

models.py:

# filepath: movies/models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()


class Movie(models.Model):
    """Film-Model"""
    title = models.CharField(max_length=200, unique=True)
    year = models.IntegerField()
    genre = models.CharField(max_length=100)
    rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
    description = models.TextField(blank=True)
    duration = models.DurationField(null=True, blank=True)
    
    # Meta
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    
    # Stats
    view_count = models.IntegerField(default=0)
    like_count = models.IntegerField(default=0)
    
    class Meta:
        ordering = ['-year', 'title']
        indexes = [
            models.Index(fields=['year']),
            models.Index(fields=['genre']),
        ]
    
    def __str__(self):
        return f"{self.title} ({self.year})"


class Artist(models.Model):
    """Künstler-Model"""
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birth_date = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return f"{self.first_name} {self.last_name}"


class MovieCasting(models.Model):
    """Besetzung-Model"""
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='castings')
    role_name = models.CharField(max_length=200)
    is_lead = models.BooleanField(default=False)
    
    class Meta:
        unique_together = ['movie', 'artist', 'role_name']
        ordering = ['-is_lead', 'role_name']
    
    def __str__(self):
        return f"{self.artist} as {self.role_name} in {self.movie}"

validators.py:

# filepath: movies/validators.py
from rest_framework import serializers


def validate_year_range(value):
    """Jahr muss zwischen 1888 und 2100 liegen"""
    if value < 1888:
        raise serializers.ValidationError(
            "Erstes Film war 1888 - Jahr zu früh!"
        )
    if value > 2100:
        raise serializers.ValidationError(
            f"Jahr {value} ist zu weit in der Zukunft!"
        )
    return value


def validate_rating_range(value):
    """Rating muss zwischen 0 und 10 liegen"""
    if value is not None and (value < 0 or value > 10):
        raise serializers.ValidationError(
            f"Rating muss zwischen 0 und 10 liegen, nicht {value}"
        )
    return value

fields.py:

# filepath: movies/fields.py
from rest_framework import serializers
from datetime import timedelta
import re


class DurationField(serializers.Field):
    """Custom Field für Film-Dauer"""
    
    def to_representation(self, value):
        """timedelta → String (z.B. "2h 16min")"""
        if value is None:
            return None
        
        total_minutes = int(value.total_seconds() / 60)
        hours = total_minutes // 60
        minutes = total_minutes % 60
        
        if hours > 0:
            return f"{hours}h {minutes}min"
        else:
            return f"{minutes}min"
    
    def to_internal_value(self, data):
        """String → timedelta"""
        if data is None:
            return None
        
        # Parse "120min" oder "2h 30min"
        pattern = r'(?:(\d+)h\s*)?(?:(\d+)min)?'
        match = re.match(pattern, str(data))
        
        if not match:
            raise serializers.ValidationError(
                "Format muss sein: '120min' oder '2h 30min'"
            )
        
        hours = int(match.group(1) or 0)
        minutes = int(match.group(2) or 0)
        total_minutes = hours * 60 + minutes
        
        if total_minutes <= 0:
            raise serializers.ValidationError("Dauer muss größer als 0 sein")
        
        return timedelta(minutes=total_minutes)

📦 Komplettes Beispiel - Serializers

serializers.py (Komplett):

# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
from .validators import validate_year_range, validate_rating_range
from .fields import DurationField
from datetime import datetime
from django.db.models import Count


# ============================================
# BASE SERIALIZERS
# ============================================

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """Basis mit Dynamic Fields Support"""
    
    def __init__(self, *args, **kwargs):
        context = kwargs.get('context', {})
        request = context.get('request')
        
        super().__init__(*args, **kwargs)
        
        if not request:
            return
        
        # ?fields=id,title
        fields_param = request.query_params.get('fields')
        if fields_param:
            fields = fields_param.split(',')
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)
        
        # ?exclude=description
        exclude_param = request.query_params.get('exclude')
        if exclude_param:
            exclude = exclude_param.split(',')
            for field_name in exclude:
                self.fields.pop(field_name, None)


# ============================================
# ARTIST SERIALIZERS
# ============================================

class ArtistSerializer(serializers.ModelSerializer):
    """Artist Serializer"""
    full_name = serializers.SerializerMethodField()
    age = serializers.SerializerMethodField()
    
    class Meta:
        model = Artist
        fields = ['id', 'first_name', 'last_name', 'full_name', 'birth_date', 'age']
        read_only_fields = ['id']
    
    def get_full_name(self, obj):
        return f"{obj.first_name} {obj.last_name}"
    
    def get_age(self, obj):
        if not obj.birth_date:
            return None
        today = datetime.now().date()
        return today.year - obj.birth_date.year


# ============================================
# CASTING SERIALIZERS
# ============================================

class MovieCastingListSerializer(serializers.ModelSerializer):
    """Einfacher Casting Serializer für Listen"""
    artist_name = serializers.CharField(source='artist.full_name', read_only=True)
    
    class Meta:
        model = MovieCasting
        fields = ['id', 'artist_name', 'role_name', 'is_lead']


class MovieCastingDetailSerializer(serializers.ModelSerializer):
    """Detaillierter Casting Serializer"""
    artist = ArtistSerializer(read_only=True)
    
    # Für Write
    artist_id = serializers.PrimaryKeyRelatedField(
        source='artist',
        queryset=Artist.objects.all(),
        write_only=True
    )
    
    class Meta:
        model = MovieCasting
        fields = ['id', 'artist', 'artist_id', 'role_name', 'is_lead']
        read_only_fields = ['id']


# ============================================
# MOVIE SERIALIZERS
# ============================================

class MovieListSerializer(DynamicFieldsModelSerializer):
    """
    Movie List Serializer - Minimal für Übersichten
    
    Features:
    - Nur wichtigste Felder
    - Berechnetes Alter
    - Rating-Kategorie
    - Dynamic Fields Support
    """
    age = serializers.SerializerMethodField()
    rating_category = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = [
            'id', 'title', 'year', 'genre', 'rating',
            'age', 'rating_category'
        ]
    
    def get_age(self, obj):
        """Alter des Films"""
        return datetime.now().year - obj.year
    
    def get_rating_category(self, obj):
        """Rating-Kategorie"""
        if not obj.rating:
            return "Nicht bewertet"
        
        rating = float(obj.rating)
        if rating >= 9.0:
            return "Meisterwerk"
        elif rating >= 8.0:
            return "Ausgezeichnet"
        elif rating >= 7.0:
            return "Gut"
        elif rating >= 6.0:
            return "Durchschnittlich"
        else:
            return "Schwach"


class MovieDetailSerializer(DynamicFieldsModelSerializer):
    """
    Movie Detail Serializer - Maximal für Einzelansicht
    
    Features:
    - Alle Felder
    - Nested Castings
    - Berechnete Felder
    - Stats
    - Dynamic Fields Support
    """
    # Nested Relations
    castings = MovieCastingDetailSerializer(many=True, read_only=True)
    created_by_name = serializers.CharField(
        source='created_by.username',
        read_only=True
    )
    
    # Custom Fields
    duration = DurationField()
    
    # SerializerMethodFields
    age = serializers.SerializerMethodField()
    is_classic = serializers.SerializerMethodField()
    rating_category = serializers.SerializerMethodField()
    lead_actors = serializers.SerializerMethodField()
    url = serializers.SerializerMethodField()
    
    # Stats (aus Context)
    stats = serializers.SerializerMethodField()
    
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = [
            'id', 'created_at', 'updated_at', 
            'created_by', 'view_count', 'like_count'
        ]
    
    def get_age(self, obj):
        return datetime.now().year - obj.year
    
    def get_is_classic(self, obj):
        return self.get_age(obj) > 30
    
    def get_rating_category(self, obj):
        if not obj.rating:
            return "Nicht bewertet"
        rating = float(obj.rating)
        if rating >= 9.0: return "Meisterwerk"
        elif rating >= 8.0: return "Ausgezeichnet"
        elif rating >= 7.0: return "Gut"
        elif rating >= 6.0: return "Durchschnittlich"
        else: return "Schwach"
    
    def get_lead_actors(self, obj):
        """Hauptdarsteller"""
        lead_castings = obj.castings.filter(is_lead=True)[:3]
        return [
            {
                'name': f"{c.artist.first_name} {c.artist.last_name}",
                'role': c.role_name
            }
            for c in lead_castings
        ]
    
    def get_url(self, obj):
        """Absolute URL"""
        request = self.context.get('request')
        if not request:
            return None
        from django.urls import reverse
        path = reverse('movie-detail', kwargs={'pk': obj.pk})
        return request.build_absolute_uri(path)
    
    def get_stats(self, obj):
        """Stats nur wenn angefordert"""
        if not self.context.get('include_stats'):
            return None
        return {
            'views': obj.view_count,
            'likes': obj.like_count,
            'casting_count': obj.castings.count()
        }


class MovieWriteSerializer(serializers.ModelSerializer):
    """
    Movie Write Serializer - Nur für Create/Update
    
    Features:
    - Nur beschreibbare Felder
    - Umfassende Validierung
    - Custom Fields
    - Normalisierung
    """
    # Custom Validators
    year = serializers.IntegerField(validators=[validate_year_range])
    rating = serializers.DecimalField(
        max_digits=3,
        decimal_places=1,
        validators=[validate_rating_range],
        required=False,
        allow_null=True
    )
    
    # Custom Fields
    duration = DurationField(required=False, allow_null=True)
    
    class Meta:
        model = Movie
        fields = ['title', 'year', 'genre', 'rating', 'description', 'duration']
        extra_kwargs = {
            'title': {
                'required': True,
                'min_length': 2,
                'max_length': 200
            },
            'description': {
                'required': False,
                'allow_blank': True
            }
        }
    
    def validate_title(self, value):
        """Titel validieren"""
        # Trimmen
        value = value.strip()
        
        # Unique check (bei Update eigenen Film ausschließen)
        queryset = Movie.objects.filter(title__iexact=value)
        if self.instance:
            queryset = queryset.exclude(pk=self.instance.pk)
        
        if queryset.exists():
            raise serializers.ValidationError(
                f"Film mit Titel '{value}' existiert bereits!"
            )
        
        return value
    
    def validate_genre(self, value):
        """Genre validieren"""
        allowed_genres = [
            'Action', 'Comedy', 'Drama', 'Horror',
            'Sci-Fi', 'Romance', 'Thriller', 'Documentary'
        ]
        
        if value and value not in allowed_genres:
            raise serializers.ValidationError(
                f"Genre muss eines sein von: {', '.join(allowed_genres)}"
            )
        
        return value
    
    def validate(self, attrs):
        """Object-level Validierung"""
        year = attrs.get('year')
        rating = attrs.get('rating')
        
        # Jahr nicht in Zukunft
        current_year = datetime.now().year
        if year and year > current_year + 1:
            raise serializers.ValidationError({
                'year': f'Jahr darf nicht mehr als {current_year + 1} sein'
            })
        
        # Alte Filme max 9.5
        if year and year < 1920 and rating and rating > 9.5:
            raise serializers.ValidationError(
                'Stummfilme vor 1920 können maximal 9.5 Rating haben'
            )
        
        return attrs
    
    def to_internal_value(self, data):
        """Input normalisieren"""
        data = data.copy()
        
        # Titel normalisieren
        if 'title' in data:
            data['title'] = data['title'].strip()
        
        # Genre normalisieren
        if 'genre' in data:
            genre_mapping = {
                'scifi': 'Sci-Fi',
                'sci fi': 'Sci-Fi',
            }
            data['genre'] = genre_mapping.get(
                data['genre'].lower(),
                data['genre']
            )
        
        return super().to_internal_value(data)
    
    def create(self, validated_data):
        """Custom Create"""
        # User aus Context
        request = self.context.get('request')
        if request and hasattr(request, 'user'):
            validated_data['created_by'] = request.user
        
        return super().create(validated_data)

🎯 Komplettes Beispiel - Views

views.py (Komplett):

# filepath: movies/views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Prefetch

from .models import Movie, Artist, MovieCasting
from .serializers import (
    MovieListSerializer,
    MovieDetailSerializer,
    MovieWriteSerializer,
    ArtistSerializer,
    MovieCastingDetailSerializer
)


class MovieViewSet(viewsets.ModelViewSet):
    """
    ViewSet für Movies mit allen Features
    
    Features:
    - Verschiedene Serializers für List/Detail/Write
    - Filtering & Search
    - Ordering
    - Performance-Optimierung
    - Custom Actions
    - Dynamic Fields
    - Stats on-demand
    """
    queryset = Movie.objects.all()
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    # Filtering
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter
    ]
    filterset_fields = ['year', 'genre']
    search_fields = ['title', 'description']
    ordering_fields = ['year', 'rating', 'title', 'created_at']
    ordering = ['-year']
    
    def get_queryset(self):
        """QuerySet optimieren"""
        queryset = Movie.objects.all()
        
        # Action-spezifische Optimierung
        if self.action == 'list':
            # Liste: Nur nötige Felder
            queryset = queryset.only(
                'id', 'title', 'year', 'genre', 'rating'
            )
        
        elif self.action == 'retrieve':
            # Detail: Alles + Related
            queryset = queryset.select_related(
                'created_by'
            ).prefetch_related(
                Prefetch(
                    'castings',
                    queryset=MovieCasting.objects.select_related('artist')
                )
            )
        
        # Custom Filters
        year_min = self.request.query_params.get('year_min')
        if year_min:
            queryset = queryset.filter(year__gte=year_min)
        
        year_max = self.request.query_params.get('year_max')
        if year_max:
            queryset = queryset.filter(year__lte=year_max)
        
        return queryset
    
    def get_serializer_class(self):
        """Dynamisch Serializer wählen"""
        if self.action == 'list':
            return MovieListSerializer
        elif self.action in ['create', 'update', 'partial_update']:
            return MovieWriteSerializer
        else:
            return MovieDetailSerializer
    
    def get_serializer_context(self):
        """Context erweitern"""
        context = super().get_serializer_context()
        
        # Stats on-demand
        context['include_stats'] = self.request.query_params.get(
            'stats', 
            'false'
        ).lower() == 'true'
        
        return context
    
    @action(detail=True, methods=['post'])
    def add_casting(self, request, pk=None):
        """Custom Action: Casting hinzufügen"""
        movie = self.get_object()
        
        serializer = MovieCastingDetailSerializer(
            data=request.data,
            context={'request': request}
        )
        
        if serializer.is_valid():
            serializer.save(movie=movie)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    @action(detail=True, methods=['get'])
    def stats(self, request, pk=None):
        """Custom Action: Detaillierte Stats"""
        movie = self.get_object()
        
        return Response({
            'views': movie.view_count,
            'likes': movie.like_count,
            'castings': {
                'total': movie.castings.count(),
                'leads': movie.castings.filter(is_lead=True).count()
            },
            'age': 2024 - movie.year,
            'rating': str(movie.rating) if movie.rating else None
        })
    
    @action(detail=False, methods=['get'])
    def top_rated(self, request):
        """Custom Action: Top-bewertete Filme"""
        top_movies = self.get_queryset().filter(
            rating__isnull=False
        ).order_by('-rating')[:10]
        
        serializer = self.get_serializer(top_movies, many=True)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def recent(self, request):
        """Custom Action: Neueste Filme"""
        recent_movies = self.get_queryset().order_by('-year', '-created_at')[:20]
        
        serializer = self.get_serializer(recent_movies, many=True)
        return Response(serializer.data)


class ArtistViewSet(viewsets.ModelViewSet):
    """ViewSet für Artists"""
    queryset = Artist.objects.all()
    serializer_class = ArtistSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['first_name', 'last_name']
    ordering_fields = ['last_name', 'birth_date']
    ordering = ['last_name']

🎯 Zusammenfassung

Was haben wir gelernt?

📜 Serializer Basics

  • ✅ Was sind Serializer?
  • ✅ Serializer vs ModelSerializer
  • ✅ fields, exclude, read_only_fields
  • ✅ extra_kwargs, depth

✅ Validierung

  • ✅ Field-Level Validation
  • ✅ Object-Level Validation
  • ✅ Custom Validators
  • ✅ Error Handling

🔗 Relations

  • ✅ PrimaryKeyRelatedField
  • ✅ StringRelatedField
  • ✅ SlugRelatedField
  • ✅ HyperlinkedRelatedField
  • ✅ Nested Serializers

🎨 Advanced

  • ✅ SerializerMethodField
  • ✅ Custom Fields
  • ✅ to_representation()
  • ✅ to_internal_value()
  • ✅ Dynamic Fields
  • ✅ Serializer Context

⚡ Performance

  • ✅ Prefetch & Select Related
  • ✅ Annotate für Berechnungen
  • ✅ N+1 Queries vermeiden
  • ✅ Read vs Write Serializers

💡 Best Practices

  • ✅ DRY Principle
  • ✅ Single Responsibility
  • ✅ Security (read_only_fields)
  • ✅ Documentation
  • ✅ Separate Serializers

📊 Vergleich - Alle Serializer-Typen

🔧 Serializer (Basis)

~50 Zeilen Code

class MovieSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    year = serializers.IntegerField()
    rating = serializers.DecimalField(
        max_digits=3, 
        decimal_places=1
    )
    
    def create(self, validated_data):
        return Movie.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        instance.title = validated_data.get('title', instance.title)
        instance.year = validated_data.get('year', instance.year)
        instance.rating = validated_data.get('rating', instance.rating)
        instance.save()
        return instance

✅ Vorteile:

  • Volle Kontrolle
  • Für non-Model Daten

❌ Nachteile:

  • Viel Code
  • Wiederholungen

🎯 ModelSerializer (Standard)

~5 Zeilen Code

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at']

# Fertig! create() und update() automatisch!

✅ Vorteile:

  • Minimaler Code
  • Model-basiert (DRY)
  • create/update automatisch

❌ Nachteile:

  • Nur für Models

🎨 ModelSerializer + Custom

~20 Zeilen Code

class MovieSerializer(serializers.ModelSerializer):
    age = serializers.SerializerMethodField()
    castings = MovieCastingSerializer(many=True, read_only=True)
    
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at']
        extra_kwargs = {
            'title': {'min_length': 2}
        }
    
    def get_age(self, obj):
        return datetime.now().year - obj.year
    
    def validate_year(self, value):
        if value < 1888:
            raise serializers.ValidationError("Too early")
        return value

# Beste Balance!

✅ Vorteile:

  • Automatische Basis + Custom
  • Flexibel & lesbar
  • Production-Ready

⭐ Empfohlen für 95% der Fälle!

⚠️ Common Pitfalls - Häufige Fehler

❌ Fehler 1: N+1 Queries

# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
    casting_count = serializers.SerializerMethodField()
    
    def get_casting_count(self, obj):
        return obj.castings.count()  # ← Query pro Movie!

# Bei 100 Movies = 101 Queries! 😱

# BESSER:
# View:
def get_queryset(self):
    return Movie.objects.annotate(
        casting_count_db=Count('castings')
    )

# Serializer:
casting_count = serializers.IntegerField(
    source='casting_count_db'
)
# Nur 1 Query! 🚀

❌ Fehler 2: Zu viele Felder schreibbar

# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
    # User kann ALLES ändern: created_by, is_verified, etc.!

# BESSER:
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = [
            'id', 'created_at', 'updated_at',
            'created_by', 'is_verified', 'view_count'
        ]

# ODER: Separate Write-Serializer
class MovieWriteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = ['title', 'year', 'genre', 'rating']

❌ Fehler 3: Validierung in View

# SCHLECHT:
class MovieCreateView(APIView):
    def post(self, request):
        if not request.data.get('title'):
            return Response({'error': 'Title required'}, status=400)
        if request.data.get('year') < 1888:
            return Response({'error': 'Year too early'}, status=400)
        # ... viel Code

# BESSER:
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        extra_kwargs = {
            'title': {'required': True}
        }
    
    def validate_year(self, value):
        if value < 1888:
            raise serializers.ValidationError("Year too early")
        return value

# View wird einfach:
class MovieCreateView(APIView):
    def post(self, request):
        serializer = MovieSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

❌ Fehler 4: depth zu hoch

# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'
        depth = 3  # ← Zu tief! Huge Response!

# Response wird riesig und kann Endlos-Schleifen haben!

# BESSER:
# Keine depth, sondern explizite Nested Serializers
castings = MovieCastingSerializer(many=True, read_only=True)

🧪 Testing Serializers

Serializer testen ist wichtig!

Validierung & Transformationen prüfen

Beispiel: Movie Serializer Tests

# filepath: movies/tests/test_serializers.py
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from datetime import datetime

from movies.models import Movie, Artist, MovieCasting
from movies.serializers import MovieSerializer, MovieWriteSerializer


class MovieSerializerTest(TestCase):
    """Tests für Movie Serializer"""
    
    def setUp(self):
        """Test-Daten erstellen"""
        self.movie = Movie.objects.create(
            title="The Matrix",
            year=1999,
            genre="Sci-Fi",
            rating=8.7
        )
    
    def test_serialization(self):
        """Test: Serialisierung (Python → JSON)"""
        serializer = MovieSerializer(self.movie)
        data = serializer.data
        
        # Assertions
        self.assertEqual(data['title'], "The Matrix")
        self.assertEqual(data['year'], 1999)
        self.assertEqual(data['genre'], "Sci-Fi")
        self.assertEqual(float(data['rating']), 8.7)
        
        # Berechnete Felder
        current_year = datetime.now().year
        expected_age = current_year - 1999
        self.assertEqual(data['age'], expected_age)
    
    def test_deserialization(self):
        """Test: Deserialisierung (JSON → Python)"""
        data = {
            'title': 'Inception',
            'year': 2010,
            'genre': 'Sci-Fi',
            'rating': 8.8
        }
        
        serializer = MovieWriteSerializer(data=data)
        self.assertTrue(serializer.is_valid())
        
        movie = serializer.save()
        self.assertEqual(movie.title, "Inception")
        self.assertEqual(movie.year, 2010)
    
    def test_validation_year_too_early(self):
        """Test: Jahr zu früh"""
        data = {
            'title': 'Old Movie',
            'year': 1800,  # ← Zu früh!
            'genre': 'Drama'
        }
        
        serializer = MovieWriteSerializer(data=data)
        self.assertFalse(serializer.is_valid())
        self.assertIn('year', serializer.errors)
    
    def test_validation_title_duplicate(self):
        """Test: Titel muss unique sein"""
        data = {
            'title': 'The Matrix',  # ← Existiert schon!
            'year': 2020,
            'genre': 'Action'
        }
        
        serializer = MovieWriteSerializer(data=data)
        self.assertFalse(serializer.is_valid())
        self.assertIn('title', serializer.errors)
    
    def test_update(self):
        """Test: Update funktioniert"""
        data = {
            'title': 'The Matrix Reloaded',
            'year': 2003,
            'genre': 'Sci-Fi',
            'rating': 7.2
        }
        
        serializer = MovieWriteSerializer(self.movie, data=data)
        self.assertTrue(serializer.is_valid())
        
        updated_movie = serializer.save()
        
        self.assertEqual(updated_movie.id, self.movie.id)
        self.assertEqual(updated_movie.title, "The Matrix Reloaded")
        self.assertEqual(updated_movie.year, 2003)

🚀 Nächste Schritte

1

Authentication & Permissions

JWT Tokens, Session Auth

Custom Permissions

2

Advanced Filtering

django-filter

Custom FilterSets

3

File Uploads

ImageField, FileField

Media Storage

4

Throttling & Caching

Rate Limiting

Redis Cache

5

Testing & Deployment

API Tests

Docker Setup

📚 Ressourcen & Weiterführendes

📖 Offizielle Dokumentation

  • DRF Serializers:
    https://www.django-rest-framework.org/api-guide/serializers/
  • Model Serializer:
    https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
  • Validators:
    https://www.django-rest-framework.org/api-guide/validators/
  • Relations:
    https://www.django-rest-framework.org/api-guide/relations/

🛠️ Tools & Packages

  • drf-spectacular: OpenAPI/Swagger Docs
  • django-filter: Advanced Filtering
  • drf-yasg: Alternative Docs
  • djangorestframework-simplejwt: JWT Auth

📝 Best Practice Guides

  • Classy DRF: Browse DRF source code
    http://www.cdrf.co/
  • DRF Tutorial:
    https://www.django-rest-framework.org/tutorial/1-serialization/

🎉 Gratulation!

Du bist jetzt ein DRF Serializer Expert!

✅ Was du jetzt kannst:

  • 📦 Serializer vs ModelSerializer verstehen
  • ✅ Umfassende Validierung implementieren
  • 🔗 Relations und Nested Serializers nutzen
  • 🎨 Custom Fields erstellen
  • 🔄 to_representation() & to_internal_value()
  • 🎛️ Dynamic Fields Pattern
  • ⚡ Performance optimieren
  • 🧪 Serializer testen

🎯 Best Practices gelernt:

  • ✅ DRY Principle
  • ✅ Single Responsibility
  • ✅ Security First
  • ✅ Separate Serializers
  • ✅ Performance-Optimierung
  • ✅ Dokumentation

🚀 Nächster Schritt:

Implementiere eigene API mit allem was du gelernt hast!

  • Erstelle Models
  • Schreibe production-ready Serializers
  • Implementiere ViewSets
  • Füge Tests hinzu

Viel Erfolg mit deinen APIs! 🚀

Keep coding, keep learning! 💻

1 / 31